﻿#region Copyright and license information
// Copyright 2001-2009 Stephen Colebourne
// Copyright 2009-2011 Jon Skeet
// 
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// 
//     http://www.apache.org/licenses/LICENSE-2.0
// 
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
#endregion

using System;
using System.Xml;

namespace NodaTime.Xml
{
    /// <summary>
    ///   Extends the <see cref="XmlReader"/> to read NodaTime types.
    /// </summary>
    /// <example>
    ///   <code>
    ///   using NodaTime.Xml;
    ///   using System.Xml;
    ///   
    ///   XmlReader reader;
    /// 
    ///   while (reader.Read())
    ///   {
    ///      if (reader.IsStartElement() &amp;&amp; reader.Name == "when")
    ///      {
    ///         var when = reader.ReadElementContentAsInstant();
    ///         // Do something.
    ///      }
    ///   } 
    ///   </code>
    /// </example>
    /// <seealso cref="NodaXmlConvert"/>
    public static class XmlReaderExtensions
    {
        /// <summary>
        ///   Reads the text content at the current position as an <see cref="Instant"/>.
        /// </summary>
        /// <param name="reader">A <see cref="XmlReader"/> that is being extended.</param>
        /// <returns>The <see cref="Instant"/> equivalent of the text content at the current position.</returns>
        public static Instant ReadContentAsInstant(this XmlReader reader)
        {
            try
            {
                return NodaXmlConvert.ToInstant(reader.ReadContentAsString());
            }
            catch (FormatException e)
            {
                var lineInfo = reader as IXmlLineInfo;
                if (lineInfo == null || !lineInfo.HasLineInfo())
                    throw new XmlException(string.Format(Properties.Messages.Xml_InvalidContentForType, typeof(Instant)), e);
                throw new XmlException(string.Format(Properties.Messages.Xml_InvalidContentForType, typeof(Instant)), e, lineInfo.LineNumber, lineInfo.LinePosition);
            }
        }

        /// <summary>
        ///   Reads the current element and returns the contents as an <see cref="Instant"/>.
        /// </summary>
        /// <param name="reader">A <see cref="XmlReader"/> that is being extended.</param>
        /// <returns>The <see cref="Instant"/> equivalent of the element content.</returns>
        /// <remarks>
        ///   This method reads the start tag, the contents of the element, and moves the reader past the end element tag. 
        ///   It expands entities and ignores processing instructions and comments. The element can only contain simple content. 
        ///   The element cannot have child elements.
        /// </remarks>
        public static Instant ReadElementContentAsInstant(this XmlReader reader)
        {
            bool isEmtpty = reader.IsEmptyElement;
            reader.Read(); // Read start tag.
            if (isEmtpty || reader.NodeType == XmlNodeType.EndElement)
                return new Instant();
            var value = reader.ReadContentAsInstant();
            reader.Read(); // Read end tag.
            return value;
        }

        /// <summary>
        ///   Reads the text content at the current position as a <see cref="LocalDate"/>.
        /// </summary>
        /// <param name="reader">A <see cref="XmlReader"/> that is being extended.</param>
        /// <returns>The <see cref="LocalDate"/> equivalent of the text content at the current position.</returns>
        public static LocalDate ReadContentAsLocalDate(this XmlReader reader)
        {
            try
            {
                return NodaXmlConvert.ToLocalDate(reader.ReadContentAsString());
            }
            catch (FormatException e)
            {
                var lineInfo = reader as IXmlLineInfo;
                if (lineInfo == null || !lineInfo.HasLineInfo())
                    throw new XmlException(string.Format(Properties.Messages.Xml_InvalidContentForType, typeof(LocalDate)), e);
                throw new XmlException(string.Format(Properties.Messages.Xml_InvalidContentForType, typeof(LocalDate)), e, lineInfo.LineNumber, lineInfo.LinePosition);
            }
        }

        /// <summary>
        ///   Reads the current element and returns the contents as a <see cref="LocalDate"/>.
        /// </summary>
        /// <param name="reader">A <see cref="XmlReader"/> that is being extended.</param>
        /// <returns>The <see cref="LocalDate"/> equivalent of the element content.</returns>
        /// <remarks>
        ///   This method reads the start tag, the contents of the element, and moves the reader past the end element tag. 
        ///   It expands entities and ignores processing instructions and comments. The element can only contain simple content. 
        ///   The element cannot have child elements.
        /// </remarks>
        public static LocalDate ReadElementContentAsLocalDate(this XmlReader reader)
        {
            bool isEmtpty = reader.IsEmptyElement;
            reader.Read(); // Read start tag.
            if (isEmtpty || reader.NodeType == XmlNodeType.EndElement)
                return new LocalDate();
            var value = reader.ReadContentAsLocalDate();
            reader.Read(); // Read end tag.
            return value;
        }

        /// <summary>
        ///   Reads the text content at the current position as a <see cref="LocalDateTime"/>.
        /// </summary>
        /// <param name="reader">A <see cref="XmlReader"/> that is being extended.</param>
        /// <returns>The <see cref="LocalDateTime"/> equivalent of the text content at the current position.</returns>
        public static LocalDateTime ReadContentAsLocalDateTime(this XmlReader reader)
        {
            try
            {
                return NodaXmlConvert.ToLocalDateTime(reader.ReadContentAsString());
            }
            catch (FormatException e)
            {
                var lineInfo = reader as IXmlLineInfo;
                if (lineInfo == null || !lineInfo.HasLineInfo())
                    throw new XmlException(string.Format(Properties.Messages.Xml_InvalidContentForType, typeof(LocalDateTime)), e);
                throw new XmlException(string.Format(Properties.Messages.Xml_InvalidContentForType, typeof(LocalDateTime)), e, lineInfo.LineNumber, lineInfo.LinePosition);
            }
        }

        /// <summary>
        ///   Reads the current element and returns the contents as a <see cref="LocalDateTime"/>.
        /// </summary>
        /// <param name="reader">A <see cref="XmlReader"/> that is being extended.</param>
        /// <returns>The <see cref="LocalDateTime"/> equivalent of the element content.</returns>
        /// <remarks>
        ///   This method reads the start tag, the contents of the element, and moves the reader past the end element tag. 
        ///   It expands entities and ignores processing instructions and comments. The element can only contain simple content. 
        ///   The element cannot have child elements.
        /// </remarks>
        public static LocalDateTime ReadElementContentAsLocalDateTime(this XmlReader reader)
        {
            bool isEmtpty = reader.IsEmptyElement;
            reader.Read(); // Read start tag.
            if (isEmtpty || reader.NodeType == XmlNodeType.EndElement)
                return new LocalDateTime();
            var value = reader.ReadContentAsLocalDateTime();
            reader.Read(); // Read end tag.
            return value;
        }

        /// <summary>
        ///   Reads the text content at the current position as a <see cref="LocalTime"/>.
        /// </summary>
        /// <param name="reader">A <see cref="XmlReader"/> that is being extended.</param>
        /// <returns>The <see cref="LocalTime"/> equivalent of the text content at the current position.</returns>
        public static LocalTime ReadContentAsLocalTime(this XmlReader reader)
        {
            try
            {
                return NodaXmlConvert.ToLocalTime(reader.ReadContentAsString());
            }
            catch (FormatException e)
            {
                var lineInfo = reader as IXmlLineInfo;
                if (lineInfo == null || !lineInfo.HasLineInfo())
                    throw new XmlException(string.Format(Properties.Messages.Xml_InvalidContentForType, typeof(LocalTime)), e);
                throw new XmlException(string.Format(Properties.Messages.Xml_InvalidContentForType, typeof(LocalTime)), e, lineInfo.LineNumber, lineInfo.LinePosition);
            }
        }

        /// <summary>
        ///   Reads the current element and returns the contents as a <see cref="LocalTime"/>.
        /// </summary>
        /// <param name="reader">A <see cref="XmlReader"/> that is being extended.</param>
        /// <returns>The <see cref="LocalTime"/> equivalent of the element content.</returns>
        /// <remarks>
        ///   This method reads the start tag, the contents of the element, and moves the reader past the end element tag. 
        ///   It expands entities and ignores processing instructions and comments. The element can only contain simple content. 
        ///   The element cannot have child elements.
        /// </remarks>
        public static LocalTime ReadElementContentAsLocalTime(this XmlReader reader)
        {
            bool isEmtpty = reader.IsEmptyElement;
            reader.Read(); // Read start tag.
            if (isEmtpty || reader.NodeType == XmlNodeType.EndElement)
                return new LocalTime();
            var value = reader.ReadContentAsLocalTime();
            reader.Read(); // Read end tag.
            return value;
        }

        /// <summary>
        ///   Reads the text content at the current position as a <see cref="Duration"/>.
        /// </summary>
        /// <param name="reader">A <see cref="XmlReader"/> that is being extended.</param>
        /// <returns>The <see cref="Duration"/> equivalent of the text content at the current position.</returns>
        public static Duration ReadContentAsDuration(this XmlReader reader)
        {
            try
            {
                return NodaXmlConvert.ToDuration(reader.ReadContentAsString());
            }
            catch (FormatException e)
            {
                var lineInfo = reader as IXmlLineInfo;
                if (lineInfo == null || !lineInfo.HasLineInfo())
                    throw new XmlException(string.Format(Properties.Messages.Xml_InvalidContentForType, typeof(Duration)), e);
                throw new XmlException(string.Format(Properties.Messages.Xml_InvalidContentForType, typeof(Duration)), e, lineInfo.LineNumber, lineInfo.LinePosition);
            }
        }

        /// <summary>
        ///   Reads the current element and returns the contents as a <see cref="Duration"/>.
        /// </summary>
        /// <param name="reader">A <see cref="XmlReader"/> that is being extended.</param>
        /// <returns>The <see cref="Duration"/> equivalent of the element content.</returns>
        /// <remarks>
        ///   This method reads the start tag, the contents of the element, and moves the reader past the end element tag. 
        ///   It expands entities and ignores processing instructions and comments. The element can only contain simple content. 
        ///   The element cannot have child elements.
        /// </remarks>
        public static Duration ReadElementContentAsDuration(this XmlReader reader)
        {
            bool isEmtpty = reader.IsEmptyElement;
            reader.Read(); // Read start tag.
            if (isEmtpty || reader.NodeType == XmlNodeType.EndElement)
                return new Duration();
            var value = reader.ReadContentAsDuration();
            reader.Read(); // Read end tag.
            return value;
        }

        /// <summary>
        ///   Reads the text content at the current position as a <see cref="Period"/>.
        /// </summary>
        /// <param name="reader">A <see cref="XmlReader"/> that is being extended.</param>
        /// <returns>The <see cref="Period"/> equivalent of the text content at the current position.</returns>
        public static Period ReadContentAsPeriod(this XmlReader reader)
        {
            try
            {
                return NodaXmlConvert.ToPeriod(reader.ReadContentAsString());
            }
            catch (FormatException e)
            {
                var lineInfo = reader as IXmlLineInfo;
                if (lineInfo == null || !lineInfo.HasLineInfo())
                    throw new XmlException(string.Format(Properties.Messages.Xml_InvalidContentForType, typeof(Period)), e);
                throw new XmlException(string.Format(Properties.Messages.Xml_InvalidContentForType, typeof(Period)), e, lineInfo.LineNumber, lineInfo.LinePosition);
            }
        }

        /// <summary>
        ///   Reads the current element and returns the contents as a <see cref="Period"/>.
        /// </summary>
        /// <param name="reader">A <see cref="XmlReader"/> that is being extended.</param>
        /// <returns>The <see cref="Period"/> equivalent of the element content.</returns>
        /// <remarks>
        ///   This method reads the start tag, the contents of the element, and moves the reader past the end element tag. 
        ///   It expands entities and ignores processing instructions and comments. The element can only contain simple content. 
        ///   The element cannot have child elements.
        ///
        ///   <b>null</b> is returned when the element is empty.
        /// </remarks>
        public static Period ReadElementContentAsPeriod(this XmlReader reader)
        {
            bool isEmtpty = reader.IsEmptyElement;
            reader.Read(); // Read start tag.
            if (isEmtpty || reader.NodeType == XmlNodeType.EndElement)
                return null;
            var value = reader.ReadContentAsPeriod();
            reader.Read(); // Read end tag.
            return value;
        }

        /// <summary>
        ///   Reads the text content at the current position as a <see cref="Offset"/>.
        /// </summary>
        /// <param name="reader">A <see cref="XmlReader"/> that is being extended.</param>
        /// <returns>The <see cref="Offset"/> equivalent of the text content at the current position.</returns>
        public static Offset ReadContentAsOffset(this XmlReader reader)
        {
            try
            {
                return NodaXmlConvert.ToOffset(reader.ReadContentAsString());
            }
            catch (FormatException e)
            {
                var lineInfo = reader as IXmlLineInfo;
                if (lineInfo == null || !lineInfo.HasLineInfo())
                    throw new XmlException(string.Format(Properties.Messages.Xml_InvalidContentForType, typeof(Offset)), e);
                throw new XmlException(string.Format(Properties.Messages.Xml_InvalidContentForType, typeof(Offset)), e, lineInfo.LineNumber, lineInfo.LinePosition);
            }
        }

        /// <summary>
        ///   Reads the current element and returns the contents as a <see cref="Offset"/>.
        /// </summary>
        /// <param name="reader">A <see cref="XmlReader"/> that is being extended.</param>
        /// <returns>The <see cref="Offset"/> equivalent of the element content.</returns>
        /// <remarks>
        ///   This method reads the start tag, the contents of the element, and moves the reader past the end element tag. 
        ///   It expands entities and ignores processing instructions and comments. The element can only contain simple content. 
        ///   The element cannot have child elements.
        ///
        ///   <see cref="Offset.Zero"/> is returned when the element is empty.
        /// </remarks>
        public static Offset ReadElementContentAsOffset(this XmlReader reader)
        {
            bool isEmtpty = reader.IsEmptyElement;
            reader.Read(); // Read start tag.
            if (isEmtpty || reader.NodeType == XmlNodeType.EndElement)
                return new Offset();
            var value = reader.ReadContentAsOffset();
            reader.Read(); // Read end tag.
            return value;
        }

        /// <summary>
        ///   Reads the text content at the current position as a <see cref="OffsetDateTime"/>.
        /// </summary>
        /// <param name="reader">A <see cref="XmlReader"/> that is being extended.</param>
        /// <returns>The <see cref="OffsetDateTime"/> equivalent of the text content at the current position.</returns>
        public static OffsetDateTime ReadContentAsOffsetDateTime(this XmlReader reader)
        {
            try
            {
                return NodaXmlConvert.ToOffsetDateTime(reader.ReadContentAsString());
            }
            catch (FormatException e)
            {
                var lineInfo = reader as IXmlLineInfo;
                if (lineInfo == null || !lineInfo.HasLineInfo())
                    throw new XmlException(string.Format(Properties.Messages.Xml_InvalidContentForType, typeof(OffsetDateTime)), e);
                throw new XmlException(string.Format(Properties.Messages.Xml_InvalidContentForType, typeof(OffsetDateTime)), e, lineInfo.LineNumber, lineInfo.LinePosition);
            }
        }

        /// <summary>
        ///   Reads the current element and returns the contents as a <see cref="OffsetDateTime"/>.
        /// </summary>
        /// <param name="reader">A <see cref="XmlReader"/> that is being extended.</param>
        /// <returns>The <see cref="OffsetDateTime"/> equivalent of the element content.</returns>
        /// <remarks>
        ///   This method reads the start tag, the contents of the element, and moves the reader past the end element tag. 
        ///   It expands entities and ignores processing instructions and comments. The element can only contain simple content. 
        ///   The element cannot have child elements.
        ///
        ///   A default value is returned when the element is empty.
        /// </remarks>
        public static OffsetDateTime ReadElementContentAsOffsetDateTime(this XmlReader reader)
        {
            bool isEmtpty = reader.IsEmptyElement;
            reader.Read(); // Read start tag.
            if (isEmtpty || reader.NodeType == XmlNodeType.EndElement)
                return new OffsetDateTime();
            var value = reader.ReadContentAsOffsetDateTime();
            reader.Read(); // Read end tag.
            return value;
        }

        /// <summary>
        ///   Reads the text content at the current position as a <see cref="DateTimeZone"/>.
        /// </summary>
        /// <param name="reader">A <see cref="XmlReader"/> that is being extended.</param>
        /// <returns>The <see cref="DateTimeZone"/> equivalent of the text content at the current position.</returns>
        public static DateTimeZone ReadContentAsDateTimeZone(this XmlReader reader)
        {
            try
            {
                return NodaXmlConvert.ToDateTimeZone(reader.ReadContentAsString());
            }
            catch (FormatException e)
            {
                var lineInfo = reader as IXmlLineInfo;
                if (lineInfo == null || !lineInfo.HasLineInfo())
                    throw new XmlException(string.Format(Properties.Messages.Xml_InvalidContentForType, typeof(DateTimeZone)), e);
                throw new XmlException(string.Format(Properties.Messages.Xml_InvalidContentForType, typeof(DateTimeZone)), e, lineInfo.LineNumber, lineInfo.LinePosition);
            }
        }

        /// <summary>
        ///   Reads the current element and returns the contents as a <see cref="DateTimeZone"/>.
        /// </summary>
        /// <param name="reader">A <see cref="XmlReader"/> that is being extended.</param>
        /// <returns>The <see cref="DateTimeZone"/> equivalent of the element content.</returns>
        /// <remarks>
        ///   This method reads the start tag, the contents of the element, and moves the reader past the end element tag. 
        ///   It expands entities and ignores processing instructions and comments. The element can only contain simple content. 
        ///   The element cannot have child elements.
        ///
        ///   A default value is returned when the element is empty.
        /// </remarks>
        public static DateTimeZone ReadElementContentAsDateTimeZone(this XmlReader reader)
        {
            bool isEmtpty = reader.IsEmptyElement;
            reader.Read(); // Read start tag.
            if (isEmtpty || reader.NodeType == XmlNodeType.EndElement)
                return DateTimeZone.Utc;
            var value = reader.ReadContentAsDateTimeZone();
            reader.Read(); // Read end tag.
            return value;
        }

        /// <summary>
        ///   Reads the text content at the current position as a <see cref="RecurrenceRule"/>.
        /// </summary>
        /// <param name="reader">A <see cref="XmlReader"/> that is being extended.</param>
        /// <returns>The <see cref="RecurrenceRule"/> equivalent of the text content at the current position.</returns>
        public static RecurrenceRule ReadContentAsRecurrenceRule(this XmlReader reader)
        {
            try
            {
                return NodaXmlConvert.ToRecurrenceRule(reader.ReadContentAsString());
            }
            catch (FormatException e)
            {
                var lineInfo = reader as IXmlLineInfo;
                if (lineInfo == null || !lineInfo.HasLineInfo())
                    throw new XmlException(string.Format(Properties.Messages.Xml_InvalidContentForType, typeof(RecurrenceRule)), e);
                throw new XmlException(string.Format(Properties.Messages.Xml_InvalidContentForType, typeof(RecurrenceRule)), e, lineInfo.LineNumber, lineInfo.LinePosition);
            }
        }

        /// <summary>
        ///   Reads the current element and returns the contents as a <see cref="RecurrenceRule"/>.
        /// </summary>
        /// <param name="reader">A <see cref="XmlReader"/> that is being extended.</param>
        /// <returns>The <see cref="RecurrenceRule"/> equivalent of the element content.</returns>
        /// <remarks>
        ///   This method reads the start tag, the contents of the element, and moves the reader past the end element tag. 
        ///   It expands entities and ignores processing instructions and comments. The element can only contain simple content. 
        ///   The element cannot have child elements.
        /// </remarks>
        public static RecurrenceRule ReadElementContentAsRecurrenceRule(this XmlReader reader)
        {
            bool isEmtpty = reader.IsEmptyElement;
            reader.Read(); // Read start tag.
            if (isEmtpty || reader.NodeType == XmlNodeType.EndElement)
                return new RecurrenceRule();
            var value = reader.ReadContentAsRecurrenceRule();
            reader.Read(); // Read end tag.
            return value;
        }

        /// <summary>
        ///   Reads the text content at the current position as a <see cref="Interval"/>.
        /// </summary>
        /// <param name="reader">A <see cref="XmlReader"/> that is being extended.</param>
        /// <returns>The <see cref="Interval"/> equivalent of the text content at the current position.</returns>
        public static Interval ReadContentAsInterval(this XmlReader reader)
        {
            try
            {
                return NodaXmlConvert.ToInterval(reader.ReadContentAsString());
            }
            catch (FormatException e)
            {
                var lineInfo = reader as IXmlLineInfo;
                if (lineInfo == null || !lineInfo.HasLineInfo())
                    throw new XmlException(string.Format(Properties.Messages.Xml_InvalidContentForType, typeof(Interval)), e);
                throw new XmlException(string.Format(Properties.Messages.Xml_InvalidContentForType, typeof(Interval)), e, lineInfo.LineNumber, lineInfo.LinePosition);
            }
        }

        /// <summary>
        ///   Reads the current element and returns the contents as a <see cref="Interval"/>.
        /// </summary>
        /// <param name="reader">A <see cref="XmlReader"/> that is being extended.</param>
        /// <returns>The <see cref="Interval"/> equivalent of the element content.</returns>
        /// <remarks>
        ///   This method reads the start tag, the contents of the element, and moves the reader past the end element tag. 
        ///   It expands entities and ignores processing instructions and comments. The element can only contain simple content. 
        ///   The element cannot have child elements.
        /// </remarks>
        public static Interval ReadElementContentAsInterval(this XmlReader reader)
        {
          bool isEmtpty = reader.IsEmptyElement;
          reader.Read(); // Read start tag.
          if (isEmtpty || reader.NodeType == XmlNodeType.EndElement)
              return new Interval();
          var value = reader.ReadContentAsInterval();
          reader.Read(); // Read end tag.
          return value;
        }

        /// <summary>
        ///   Reads the text content at the current position as a <see cref="ZonedDateTime"/>.
        /// </summary>
        /// <param name="reader">A <see cref="XmlReader"/> that is being extended.</param>
        /// <returns>The <see cref="ZonedDateTime"/> equivalent of the text content at the current position.</returns>
        public static ZonedDateTime ReadContentAsZonedDateTime(this XmlReader reader)
        {
            try
            {
                return NodaXmlConvert.ToZonedDateTime(reader.ReadContentAsString());
            }
            catch (FormatException e)
            {
                var lineInfo = reader as IXmlLineInfo;
                if (lineInfo == null || !lineInfo.HasLineInfo())
                    throw new XmlException(string.Format(Properties.Messages.Xml_InvalidContentForType, typeof(ZonedDateTime)), e);
                throw new XmlException(string.Format(Properties.Messages.Xml_InvalidContentForType, typeof(ZonedDateTime)), e, lineInfo.LineNumber, lineInfo.LinePosition);
            }
        }

        /// <summary>
        ///   Reads the current element and returns the contents as a <see cref="ZonedDateTime"/>.
        /// </summary>
        /// <param name="reader">A <see cref="XmlReader"/> that is being extended.</param>
        /// <returns>The <see cref="ZonedDateTime"/> equivalent of the element content.</returns>
        /// <remarks>
        ///   This method reads the start tag, the contents of the element, and moves the reader past the end element tag. 
        ///   It expands entities and ignores processing instructions and comments. The element can only contain simple content. 
        ///   The element cannot have child elements.
        /// </remarks>
        public static ZonedDateTime ReadElementContentAsZonedDateTime(this XmlReader reader)
        {
            bool isEmtpty = reader.IsEmptyElement;
            reader.Read(); // Read start tag.
            if (isEmtpty || reader.NodeType == XmlNodeType.EndElement)
                return new ZonedDateTime();
            var value = reader.ReadContentAsZonedDateTime();
            reader.Read(); // Read end tag.
            return value;
        }
    }
}
